<?php
/**
 * @author Kirill Egorov 2011  DVelum project http://code.google.com/p/phpveil/ , http://dvelum.net
 * @package Db_Object
 * @uses  Config , Vc , Data_Filter , Zend_Db_Adapter
 */
class Db_Object
{     
    static protected $_propertySets;
    static protected $_configPath = null;

     /**
     * DB connector
     * @var Zend_DB_Adapter_Abstract
     */
    static protected $_db = null;
    static protected $_dbPrefix='';
    static protected $_linksObject = null;
    
    protected $_name;
    
    /**
     * @var Db_Object_Config
     */
    protected $_config;
    
    protected $_properties;
    
    protected $_id;
    protected $_data = array();
    protected $_updates = array();

    /**
     * @param string $name
     * @param integer $id - optional               
     * @throws Exception
     */
    public function __construct($name, $id = false)
    {      
        if(is_null(self::$_configPath))
            throw new Exception('Object. Invalid config path');
    
        $this->_name = strtolower($name);
        $this->_id = $id;        

        $this->_config = Db_Object_Config::getInstance($name);
   
       if($this->_id)
           $this->_loadData();
    }
    /**
     * Set config files path
     * @param string $path
     * @throws Exception
     * @return void
     */
    static public function setConfigPath($path)
    {
        if(is_dir($path))
            self::$_configPath = $path;
        else 
            throw new Exception('Object::setConfigPath Invalid path' . $path);    
    }
    /**
     * Set object used as links storage
     * @param string $name
     */
    static public function setLinksObject($name)
    {
    	self::$_linksObject = $name;
    }
    /**
     * Get links object
     * @return string
     */
    static public function getLinksObject()
    {
    	return self::$_linksObject;
    }
   
    /**
     * Get config files path
     * @return string
     */
    static public function getConfigPath()
    {
        return self::$_configPath;
    }
    /**
     * Get Db Adapter
     * @return Zend_Db_Adapter_Abstract
     */
    static public function getDbConnection()
    {
        return self::$_db;
    }
    /**
     * Set database table prefix
     * @param string $prefix
     */
    static public function setDbPrefix($prefix)
    {
        self::$_dbPrefix = $prefix;
    }   
    /**
     * Get database table prefix
     * @return string
     */
    static public function getDbPrefix()
    {
        return self::$_dbPrefix;
    }  
    /**
     * Set databese connection
     * @param Zend_Db_Adapter_Abstract $conn
     * @return void
     */
    static public function setDbConnection($conn)
    {
        self::$_db = $conn;
    } 
    /**
     * Load object data
     * @throws Exception
     */
    protected function _loadData()
    {
       $data =  self::$_db->fetchRow(self::$_db->select()->from($this->getTable())->where('id =?' , $this->_id));
       
       if(empty($data))
            throw new Exception('Cannot find object');
            
       unset($data['id']);
       
       foreach ($data as $field=>&$value)
       {
           if($this->_config->isMultiLink($field) && strlen($value))
               $value = unserialize($value);
               
           if($this->getConfig()->isBoolean($field)){
               if($value)
                   $value = true;
               else 
                   $value = false;
           }
               
       } unset($value);
       $this->_data = $data;     
    }
    /**
     * Get list of  fields
     * @return array
     */
    public function getFields()
    {
        return array_keys($this->_config->get('fields'));
    } 
    /**
     * Get commited values
     * @return array
     */
    public function getData()
    {
    	$data = $this->_data;
    	$data['id'] = $this->_id;
        return $data;
    }
    /**
     * Get object name
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }
    /**
     * Get object ID
     * @return integer
     */
    public function getId()
    {
        return $this->_id;
    }
    /**
     * Get full table name (with prefix)
     * @return string
     */
    public function getTable()
    {
        return self::$_dbPrefix . $this->_config->get('table');
    }
    /**
     * Check if object has updates
     * @return boolean
     */
    public function hasUpdates()
    {
         return !empty($this->_updates);
    }
    /**
     * Get object config
     * @return Db_Object_Config
     */
    public function getConfig()
    {
        return $this->_config;
    }
    
    public function getUpdates()
    {
        return $this->_updates;
    }
     
    public function setId($id){
        $this->_id = (integer) $id;
    }
 
    public function commitChanges()
    {
        if(empty($this->_updates))
            return;

         foreach ($this->_updates as $k=>$v)
             $this->_data[$k] = $v;          
    }
    /**
     * Check if field exists in config
     * @param string $name
     * @return boolean
     */
    public function fieldExists($name)
    {
        return $this->_config->fieldExists($name);
    }
    
    /**
     * Get linked object name for multilink field
     * @param string $field - field name
     * @return string
     */
    public function getLinkedObject($field)
    {
        if(!$this->_config->isMultiLink($field))
            return false;
                        
        $cfg = $this->_config->getFieldConfig($field);
        return $cfg['link_config']['object'];
    }
    /**
     * Validate link field
     * @param string $name
     * @param mixed $value
     * @return boolean
     */
    protected  function _validateLink($name , $value)
    {  
        $propConf = $this->_config->getFieldConfig($name);
        switch ($propConf['link_config']['link_type'])
        {
            case 'object':
            case 'multy':
                   return self::objectExists($propConf['link_config']['object'], $value);
                break;    
            case 'dictionary':
            	$dictionary = Dictionary::getInstance($propConf['link_config']['object']);
            	return $dictionary->isValidKey($value);
            	break;    
        }
    }
    /**
     * Check if linked objects exists
     * @param string $name
     * @param mixed integer/array $ids   
     * @return boolean
     */
    static public function objectExists($name , $ids)
    {
        $obj = new Db_Object($name);
        $table = $obj->getTable();
        
        if(!is_array($ids))
            $ids = array($ids);
        
        $sql = self::$_db->select()->from($table , array('id'))->where('id IN(?)' , $ids);
        $data = self::$_db->fetchAll($sql);

        if(empty($data))
            return false;
            
        $data = Utils::fetchCol('id', $data);
       
        foreach ($ids as $v)
            if(!in_array(intval($v) , $data , true))
                 return false;
        return true; 
    }
    /**
     * Set object properties
     * @param array $values
     * @return void
     */
    public function setValues(array $values)
    {
    	if (!empty($values))
    		foreach ($values as $k => $v)
    			$this->set($k, $v);
    }
    /**
     * Set object property
     * @param string $name
     * @param mixed $value
     * @throws Exception
     */
    public function set($name , $value)
    {          
        $propConf = $this->_config->getFieldConfig($name);
        $validator = $this->getConfig()->getValidator($name);
         
       if($validator && !call_user_func_array(array($validator , 'validate') , array($value)))
           throw new Exception('Invalid value for field '. $name); 

       /*
        * Validate value by fields type in config
        */    
       if($this->_config->isMultiLink($name))
       {
            if(is_array($value) && !empty($value[0])){
                 if(!$this->_validateLink($name , $value))
                     throw new Exception('Invalid property value');
                  $value = $this->_collectLinksData($name , $value);
            } else {
                $value = '';
            }
        } 
        elseif ($this->_config->isDictionaryLink($name))
        {
        	if($this->_config->isRequired($name) && !strlen($value))
        		throw new Exception('Field '. $name.' cannot be empty');
        		
        	if(!$this->_validateLink($name , $value))
                throw new Exception('Invalid dictionary value');
        }
        elseif ($this->_config->isLink($name))
        {
        	if(is_object($value))
        		$value = $value->__toString();
 
        	if($this->_config->isRequired($name) && !strlen($value))
        		throw new Exception('Field '. $name.' cannot be empty');
        	
        	if(strlen($value) && $value!=0 && !$this->_validateLink($name, $value))
        		throw new Exception('Invalid value for field '. $name);      		
        }
        else
        {
        	$value = Db_Object_Property::filter($propConf, $value);
        }
        
        if(isset($propConf['db_len']) && $propConf['db_len']){
            if(strlen($value) > $propConf['db_len'])
                throw new Exception('Invalid property value');
            if($propConf['db_type'] == 'bit' && (strlen($value) > $propConf['db_len'] || strlen($value) < $propConf['db_len']))
				throw new Exception('Invalid length for bit value');
        }
        
        if(isset($this->_data[$name] ) && $this->_data[$name]  === $value){
            if(isset($this->_updates[$name]))
                unset($this->_updates[$name]);
            return true;         
        } 
  
          
        
        $this->_updates[$name] = $value;   
    }
    
    protected function _collectLinksData($field , $ids)
    {
        $ids = array_map('intval', $ids);
        $linkedObject = new Db_Object($this->getLinkedObject($field));      
        /*
         * Init object model
         */
        $model = Model::factory($linkedObject->getName());
        /*
         * Find title field for link
         */
        $title = 'id';
        $lt = $linkedObject->getConfig()->getLinkTitle();       
        if($lt)
            $title = $lt;
        
        $data = $model->getItems($ids , array('id' , 'title'=>$title)); 
        
        if(!empty($data))
            $data = Utils::rekey('id', $data);
            
        $result = array();    
        foreach ($ids as $v){
            if(!isset($data[$v]))
                throw new Exception('Invalid link');
                
            $result[$v] = array('id'=>$v , 'title'=>$data[$v]['title']);    
        }     
        return $result;      
    }
    
    public function __set($key , $value)
    {
        return $this->set($key, $value);
    }
    
    public function __get($key)
    {
    	if($key==='id')
    		return $this->getId();
    		  		
    	return $this->get($key);
    }
    /**
     * Get object property value.
 	 * If field value was updated method returns new value
     * otherwise returns old value
     * @param string $name - field name
     * @throws Exception
     * @return mixed
     */
    public function get($name)
    {
         if(!$this->fieldExists($name))
            throw new Exception('Invalid property requested ['.$name.']');
            
         $value = null;  
      
         if(isset($this->_data[$name]))
              $value = $this->_data[$name];
             
         if(isset($this->_updates[$name]))
              $value = $this->_updates[$name];
                        
         if($this->_config->isMultiLink($name))
             $value = unserialize($name);   

          return $value;  
    }
    /**
     * Get old field value (before update)
     * @param string $name - field name
     * @return mixed
     */
    public function getOld($name)
    {
    	if(!$this->fieldExists($name))
                throw new Exception('Invalid property requested ['.$name.']');              
        return $this->_data[$name];     
    } 
    /**
     * Save changes
     * @param boolean $log log operation
     * @return boolean;
     */
    public function save($log = true)
    {
        $store = Db_Object_Store::getInstance();
        try {
            if(!$this->getId()){      
            	
                if($this->_properties['rev_control']){
                    $this->date_created = date('Y-m-d H:i:s'); 
                    $this->author_id = User_Admin::getInstance()->id;
                }
                $id = $store->insert($this , $log);
                $this->setId($id);             
                return (integer) $id;               
            } else {        	
                if($this->_properties['rev_control']){
                    $this->date_updated = date('Y-m-d H:i:s');
                    $this->editor_id = User_Admin::getInstance()->id;
                }            
                return (integer) $store->update($this , $log);  
            }   
        }catch (Exception $e){
            return false;
        }
    }
    /**
     * Delete object from the system
     * @return boolean - success
     */
    public function delete()
    {
    	$store = Db_Object_Store::getInstance();
        return $store->delete($this);    
    }
    /**
     * Serialize multilink properties
     * @param array $data
     */
    public function serializeLinks($data)
    {
        foreach ($data as $k=>&$v)
            if($this->_config->isMultiLink($k) && !empty($v))
                $v = serialize($v);  
                        
        return $data;
    }
    /**
     * Validate object data for unique values
     * Returns error on fist invalid group (if not false than invalid)
     * @property boolean $new
     * @return mixed false / array
     * @todo cleanup the code
     */
    public function validateUniqueValues()
    {
        $uniqGroups = array();

        foreach ($this->_config->get('fields') as $k=>$v)
        {
            if($k==='id')
                continue;
            
        	if(($k==='id' && !$this->getId()) || !isset($v['unique']) || !$v['unique'] || !empty($v['unique']) )
        		continue;
             
			if(is_array($v['unique']))
			{								
				foreach ($v['unique'] as $val){								
					if(!array_key_exists($val, $uniqGroups))
                    	$uniqGroups[$val] = array();                   	              
                 	$uniqGroups[$val][$k] = $this->get($k);
				}	
				
			}else{
				  $v['unique'] = strval($v['unique']);
				  
				  if(!array_key_exists($v['unique'], $uniqGroups))
                    	$uniqGroups[$v['unique']] = array();
				
                   $uniqGroups[$v['unique']][$k] = $this->get($k);  	
			}  
        }
        
        if(empty($uniqGroups))
            return false;
            
         foreach ($uniqGroups as $group)
         {
             $sql = self::$_db->select()
                             ->from($this->getTable() , array('count'=>'COUNT(*)'));
                                    
             if($this->getId())                
                 $sql->where(' id != ?', $this->getId());
                   
             foreach ($group as $k=>$v){
             	 if($k==='id')
             	 	continue;
                 $sql->where(self::$_db->quoteIdentifier($k) . ' =?' , $v);
             }       
                      
             $count = self::$_db->fetchOne($sql);
             if($count > 0)
             {
                 foreach ($group as $k=>&$v){
                     $v = LANG::getInstance()->SB_UNIQUE;
                 } unset($v);
                 
                 return $group;
             }        
         }   
         return false; 
    }
    /**
     * Convert object into string representation
     * @return string
     */
    public function __toString()
    {
    	return strval($this->getId());
    }
    /**
     * Factory method
     * @param string $name
     * @param integer $id
     * @return Db_Object
     */
    static public function factory($name , $id = false)
    {
    	return new Db_Object($name , $id);
    }
}